// Copyright 2019 The Lynx Authors. All rights reserved.
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.

#import <Lynx/LynxError.h>

#import <Lynx/LynxLog.h>
#import <Lynx/LynxSubErrorCode.h>
#import <Lynx/LynxVersion.h>

NSString* const LynxErrorDomain = @"com.lynx.error";

NSString* const LynxErrorUserInfoKeyMessage = @"message";
NSString* const LynxErrorUserInfoKeySourceError = @"sourceError";
NSString* const LynxErrorUserInfoKeyCustomInfo = @"customInfo";
NSString* const LynxErrorUserInfoKeyStackInfo = @"stackInfo";

// The prefix indicates that the field should be placed in the 'context' field.
NSString* const LynxErrorKeyPrefixContext = @"lynx_context_";
// The field 'context' is mainly used to store custom infos that are of interest to the user, while
// other custom infos are mainly used to assist client side debugging. LogBox will determine whether
// to display the information based on whether a custom info is present in field 'context'.
NSString* const LynxErrorKeyContext = @"context";

// Some commonly used keys of LynxError's customInfo
NSString* const LynxErrorKeyResourceType = @"type";
NSString* const LynxErrorKeyResourceUrl = @"src";

// LynxError's level
NSString* const LynxErrorLevelError = @"error";
NSString* const LynxErrorLevelWarn = @"warn";

// Suggestion for errors with a complex cause that require a detailed explanation on the official
// site
NSString* const LynxErrorSuggestionRefOfficialSite =
    @"Please refer to the solution in Doc 'LynxError FAQ' on the official website.";

// be used to construct structured error for
// Lynx. Error report interface accept error
// as a NSError and extract error message from
// NSError.userInfo. so we put the json that
// generated by error content to the userInfo.
@implementation LynxError {
  NSInteger _subCode;
  NSArray* _consumers;
  BOOL _isNewErrorCode;
  // replace userInfo of parent class, since
  // the old userInfo is readonly but we need
  // modify it when error content have changed.
  NSDictionary* _userInfo;
}

+ (instancetype)lynxErrorWithCode:(NSInteger)code message:(NSString*)errorMsg {
  return [LynxError lynxErrorWithCode:code
                              message:errorMsg
                        fixSuggestion:@""
                                level:[LynxSubErrorCodeUtils levelToStr:ELynxLevelError]
                           customInfo:nil];
}

+ (instancetype)lynxErrorWithCode:(NSInteger)code
                          message:(NSString*)errorMsg
                    fixSuggestion:(NSString*)suggestion
                            level:(NSString*)level {
  return [LynxError lynxErrorWithCode:code
                              message:errorMsg
                        fixSuggestion:suggestion
                                level:level
                           customInfo:nil];
}

+ (instancetype)lynxErrorWithCode:(NSInteger)code
                          message:(NSString*)errorMsg
                    fixSuggestion:(NSString*)suggestion
                            level:(NSString*)level
                       customInfo:(NSDictionary* _Nullable)customInfo {
  return [LynxError lynxErrorWithCode:code
                              message:errorMsg
                        fixSuggestion:suggestion
                                level:level
                           customInfo:customInfo
                         isLogBoxOnly:NO];
}

+ (instancetype)lynxErrorWithCode:(NSInteger)code
                          message:(NSString*)errorMsg
                    fixSuggestion:(NSString*)suggestion
                            level:(NSString*)level
                       customInfo:(NSDictionary* _Nullable)customInfo
                     isLogBoxOnly:(BOOL)isLogBoxOnly {
  LynxError* error = nil;
  LynxSubErrorCodeMetaData* metadata = [LynxSubErrorCodeUtils getMetaData:code];
  if (metadata) {
    error = [[LynxError alloc] initWithDomain:LynxErrorDomain code:code / 100 userInfo:nil];
    error->_fixSuggestion = metadata.fixSuggestion.length > 0 ? metadata.fixSuggestion : suggestion;
    error->_consumers = metadata.consumer;
    error->_isNewErrorCode = YES;
    if (metadata.level != ELynxLevelUndecided) {
      error->_level = [LynxSubErrorCodeUtils levelToStr:metadata.level];
    } else {
      error->_level = level;
    }
  } else {
    error = [[LynxError alloc] initWithDomain:LynxErrorDomain code:code userInfo:nil];
    error->_fixSuggestion = suggestion;
    error->_level = level;
    error->_consumers = @[];
    error->_isNewErrorCode = NO;
  }
  error->_subCode = code;
  error->_summaryMessage = errorMsg;
  error->_customInfo = customInfo ? [[NSMutableDictionary alloc] initWithDictionary:customInfo]
                                  : [[NSMutableDictionary alloc] init];
  error->_isLogBoxOnly = isLogBoxOnly;
  return error;
}

- (NSDictionary*)userInfo {
  // compatible with old interface
  NSDictionary* dictionary = [super userInfo];
  if (dictionary && [dictionary count] > 0) {
    return dictionary;
  }

  // return userInfo overridden by LynxError
  if (!_userInfo) {
    _userInfo = @{LynxErrorUserInfoKeyMessage : [self generateJsonStr]};
  }
  return _userInfo;
}

- (NSInteger)code {
  return [self errorCode];
}

- (BOOL)isFatal {
  return _level == [LynxSubErrorCodeUtils levelToStr:ELynxLevelFatal];
}

- (NSInteger)errorCode {
  if (_isNewErrorCode) {
    return _subCode / 100;
  }
  return _subCode;
}

- (NSInteger)getSubCode {
  return _subCode;
}

- (BOOL)isValid {
  return (_summaryMessage && [_summaryMessage length]) || (_callStack && [_callStack length]);
}

// if the method is called by LynxError constructed by deprecated
// constructors, the method will not affect the generating of json string
- (void)addCustomInfo:(NSString*)value forKey:(NSString*)key {
  _userInfo = nil;
  [LynxError addStringValue:value forKey:key toDictionary:_customInfo];
}

- (void)setCustomInfo:(NSDictionary*)customInfo {
  _userInfo = nil;
  _customInfo = [NSMutableDictionary dictionaryWithDictionary:customInfo];
}

- (NSDictionary*)getContextInfo {
  NSMutableDictionary* res = [[NSMutableDictionary alloc] init];
  if (!_customInfo) {
    return res;
  }
  for (NSString* key in _customInfo) {
    if ([key hasPrefix:LynxErrorKeyPrefixContext]) {
      NSString* realKey = [key substringFromIndex:LynxErrorKeyPrefixContext.length];
      [res setValue:[_customInfo objectForKey:key] forKey:realKey];
    } else if ([key isEqual:LynxErrorKeyResourceUrl]) {
      [res setValue:[_customInfo objectForKey:key] forKey:key];
    }
  }
  return res;
}

- (void)setTemplateUrl:(NSString*)cardUrl {
  _userInfo = nil;
  _templateUrl = cardUrl;
}

- (void)setCardVersion:(NSString*)cardVersion {
  _userInfo = nil;
  _cardVersion = cardVersion;
}

- (void)setCallStack:(NSString*)callStack {
  _userInfo = nil;
  _callStack = callStack;
}

- (void)setRootCause:(NSString*)rootCause {
  _userInfo = nil;
  _rootCause = rootCause;
}

- (NSString*)generateJsonStr {
  NSMutableDictionary* dictionary = [[NSMutableDictionary alloc] init];
  // put required fields
  [dictionary setValue:[NSNumber numberWithLong:[self errorCode]] forKey:@"error_code"];
  [dictionary setValue:[NSNumber numberWithLong:_subCode] forKey:@"sub_code"];
  [LynxError addStringValue:_templateUrl forKey:@"url" toDictionary:dictionary];
  [LynxError addStringValue:_summaryMessage forKey:@"error" toDictionary:dictionary];
  [LynxError addStringValue:_cardVersion forKey:@"card_version" toDictionary:dictionary];
  [LynxError addStringValue:[LynxVersion versionString] forKey:@"sdk" toDictionary:dictionary];
  [LynxError addStringValue:_level forKey:@"level" toDictionary:dictionary];
  // put optional fields
  [LynxError addStringValue:_fixSuggestion forKey:@"fix_suggestion" toDictionary:dictionary];
  [LynxError addStringValue:_callStack forKey:@"error_stack" toDictionary:dictionary];
  [LynxError addStringValue:_rootCause forKey:@"root_cause" toDictionary:dictionary];
  [dictionary setValue:_consumers forKey:@"consumers"];
  // put custom fields
  [LynxError addDictionary:_customInfo toDictionary:dictionary];
  NSError* jsonParseError;
  NSData* jsonData = [NSJSONSerialization dataWithJSONObject:dictionary
                                                     options:0
                                                       error:&jsonParseError];

  if (!jsonParseError) {
    return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
  } else {
    LLogError(@"json parse error: %@", jsonParseError.localizedDescription);
    return @"";
  }
}

- (BOOL)isJSError {
  return self.errorCode >= 200 && self.errorCode < 300;
}

- (BOOL)isLepusError {
  return self.errorCode >= 1100 && self.errorCode < 1200;
}

+ (void)addStringValue:(NSString*)value
                forKey:(NSString*)key
          toDictionary:(NSMutableDictionary*)dictionary {
  if (dictionary && value && key && value.length > 0 && key.length > 0) {
    [dictionary setValue:value forKey:key];
  }
}

+ (void)addDictionary:(NSMutableDictionary*)source toDictionary:(NSMutableDictionary*)dist {
  if (!source || source.count == 0 || !dist) {
    return;
  }
  NSMutableDictionary* contextDic = [NSMutableDictionary new];
  for (NSString* key in source) {
    if ([key hasPrefix:LynxErrorKeyPrefixContext]) {
      [contextDic setValue:[source objectForKey:key] forKey:key];
    } else {
      [dist setValue:[source objectForKey:key] forKey:key];
    }
  }
  if (contextDic.count > 0) {
    [dist setValue:contextDic forKey:LynxErrorKeyContext];
  }
}

// deprecated
+ (instancetype)lynxErrorWithCode:(NSInteger)code sourceError:(nonnull NSError*)source {
  NSDictionary* userInfo = nil;
  if (source) {
    userInfo = @{LynxErrorUserInfoKeySourceError : source};
  }
  return [LynxError lynxErrorWithCode:code userInfo:userInfo];
}

// deprecated
+ (instancetype)lynxErrorWithCode:(NSInteger)code userInfo:(NSDictionary*)userInfo {
  return [[LynxError alloc] initWithDomain:LynxErrorDomain code:code userInfo:userInfo];
}

// mainly for create NSError quickly, be careful use it construct lynx error
+ (instancetype)lynxErrorWithCode:(NSInteger)code description:(nonnull NSString*)message {
  return [[LynxError alloc] initWithDomain:LynxErrorDomain
                                      code:code
                                  userInfo:@{NSLocalizedDescriptionKey : message}];
}

@end
